day 4¶

このノートブックの実行例はこちら(HTML版)で確認できます


0. はじめに¶

ページ上部のメニューバーにある Kernel メニューをクリックし、プルダウンメニューから [Change Kernel ...] を選び、gssm2023:Python を選択してください。

ノートブック上部の右隅に表示されたカーネル名が gssm2023:Python になっていることを確認してください。


1. KHCoder のテキスト解析&分析¶

1.0 事前準備 (関数の定義)¶

以下のセルを修正せずに実行してください

In [ ]:
import warnings
warnings.simplefilter('ignore')

import random
import numpy as np

# 乱数を固定する
seed = 42
random.seed(seed)
np.random.seed(seed)

# ワードクラウドを描画する
def plot_wordcloud(word_str, width=6, height=4):

    # 必要ライブラリのインポート
    import matplotlib.pyplot as plt

    # ノートブック中にプロットするマジックコマンド
    %matplotlib inline

    # プロットの準備
    fig = plt.figure(figsize=(width, height))
    ax = fig.add_subplot(1, 1, 1)

    # 指定したプロット位置(ax)にワードクラウドを描画する
    plot_wordcloud_ax(ax, word_str)

    # プロットの仕上げ
    plt.axis("off")
    plt.tight_layout()
    plt.show()

# 指定したプロット位置(ax)にワードクラウドを描画する
def plot_wordcloud_ax(ax, word_str):

    # フォントパスを取得する
    font_path = !find ${HOME} -name "ipaexg.ttf"

    # 必要ライブラリのインポート
    import wordcloud

    # ワードクラウドを作成する
    wc = wordcloud.WordCloud(
        background_color='white',
        font_path=font_path[0],
        max_font_size=100)

    # ワードクラウドを描画する
    img = wc.generate(word_str)
    ax.imshow(img, interpolation='bilinear')


# トピックモデルによるワードクラウドを描画する
def plot_topic_model(lda, feature_names, n_top_words=20, width=10, height=4):

    # フォントパスを取得する
    font_path = !find ${HOME} -name "ipaexg.ttf"

    # 必要ライブラリのインポート
    import matplotlib.pyplot as plt
    import wordcloud

    # ノートブック中にプロットするマジックコマンド
    %matplotlib inline

    fig = plt.figure(figsize=(width, height))

    # トピックごとのループ
    for topic_idx, topic in enumerate(lda.components_):

        # トピック中で出現確率の高い頻に単語をソートし,
        # ワードクラウドに描画するためテキストを生成する
        sorted_text = ' '.join([feature_names[i] for i in topic.argsort()[:-n_top_words-1:-1]])

        # ワードクラウドを作成する
        wc = wordcloud.WordCloud(
            background_color='white',
            font_path=font_path[0],
            max_font_size=100)

        # プロット位置(ax)を選ぶ
        ax = fig.add_subplot(2, 3, topic_idx + 1)

        # ワードクラウドを描画する
        img = wc.generate(sorted_text)
        ax.imshow(img, interpolation='bilinear')
        ax.set_title(f"Topic # {topic_idx+1}:")

    # プロットの仕上げ
    plt.tight_layout()
    plt.show()


# 共起ネットワーク図を描画する (抽出語-抽出語用)
def plot_cooccur_network(df, word_counts, cutoff, width=8, height=8):

    # 必要ライブラリのインポート
    import matplotlib.pyplot as plt
    import japanize_matplotlib

    # プロットの準備
    plt.figure(figsize=(width, height))
    fig = plt.figure(figsize=(width, height))

    # プロット位置(ax)を選ぶ
    ax = fig.add_subplot(1, 1, 1)

    # 指定したプロット位置(ax)に共起ネットワーク図を描画する
    plot_cooccur_network_ax(ax, df, word_counts, cutoff)

    # プロットの仕上げ
    plt.axis("off")
    plt.show()

# 指定したプロット位置(ax)に共起ネットワーク図を描画する
def plot_cooccur_network_ax(ax, df, word_counts, cutoff):

    # 必要ライブラリのインポート
    import numpy as np
    import networkx as nx
    from networkx.algorithms import community
    from networkx.drawing.nx_agraph import graphviz_layout
    import matplotlib.pyplot as plt
    import japanize_matplotlib

    # ノートブック中にプロットするマジックコマンド
    %matplotlib inline

    # 共起行列の中身(numpy行列)を取り出す
    Xc = df.values

    # 共起行列中の最大値を求める
    Xc_max = Xc.max()

    # プロットする単語リストを取得する
    words = df.columns

    # プロットする単語の出現頻度の最大値を求める (正規化用)
    count_max = word_counts.max()

    weights_w, weights_c = [], []

    # 共起行列の要素ごとのループ (値がゼロの要素はスキップ)
    for i, j in zip(*Xc.nonzero()):
        # 対角行列でかつ値がしきい値を超えるものを保持する
        if i < j and Xc[i,j] > cutoff:
            # ノード: 一方の単語とノードの大きさ(正規化した出現頻度)を保持する
            weights_w.append((words[i], {'weight': word_counts[i] / count_max}))
            # ノード: 他方の単語とノードの大きさ(正規化した出現頻度)を保持する
            weights_w.append((words[j], {'weight': word_counts[j] / count_max}))
            # エッジ: 両端の単語を結ぶエッジの太さ(正規化した共起行列の値)を保持する
            weights_c.append((words[i], words[j], Xc[i,j] / Xc_max))

    # グラフの作成
    G = nx.Graph()
    G.add_nodes_from(weights_w)
    G.add_weighted_edges_from(weights_c)
    G.remove_nodes_from(list(nx.isolates(G)))
    # G = nx.minimum_spanning_tree(G)
    # pos = nx.spring_layout(G, k=0.3)
    pos = graphviz_layout(G, prog='neato', args='-Goverlap="scalexy" -Gsep="+6" -Gnodesep=0.8 -Gsplines="polyline" -GpackMode="graph" -Gstart={}'.format(43))

    # プロット用にノートとエッジの重みをリストに変換する
    weights_n = np.array(list(nx.get_node_attributes(G, 'weight').values()))
    weights_e = np.array(list(nx.get_edge_attributes(G, 'weight').values()))

    # サブグラフの検出
    communities = community.greedy_modularity_communities(G)
    color_map = []
    for node in G:
        for i, c in enumerate(communities):
            if node in c:
                color_map.append(i)

    # グラフの描画
    nx.draw_networkx_nodes(G, pos, node_color=color_map, alpha=0.7, cmap=plt.cm.Set2, node_size=5000 * weights_n, ax=ax)
    nx.draw_networkx_edges(G, pos, edge_color='gray', edge_cmap=plt.cm.Blues, alpha=0.7, width=3 * weights_e, ax=ax)
    nx.draw_networkx_labels(G, pos, font_family='IPAexGothic', ax=ax)
    # ax.axis('off')


# 共起ネットワークを描画する (外部変数-抽出語用)
def plot_attrs_network(df, attr_counts, word_counts, cutoff, width=8, height=8):

    # 必要ライブラリのインポート
    import numpy as np
    import networkx as nx
    import matplotlib.pyplot as plt
    import japanize_matplotlib
    from networkx.drawing.nx_agraph import graphviz_layout

    # ノートブック中にプロットするマジックコマンド
    %matplotlib inline

    # 共起行列の中身(numpy行列)を取り出す
    Xc = df.values

    # 共起行列中の最大値を求める
    Xc_max = Xc.max()

    # プロットする属性(外部変数等)リストを取得する
    attrs = list(df.index)

    # プロットする属性(外部変数等)の最大数を求める (正規化用)
    attr_count_max = attr_counts.max()

    # プロットする単語リストを取得する
    words = list(df.columns)

    # プロットする単語の出現頻度の最大値を求める (正規化用)
    word_count_max = word_counts.max()

    weights_n, weights_c = [], []

    # 共起行列の要素ごとのループ
    for i, j in zip(*Xc.nonzero()):
        # 値がしきい値を超えるものを保持する (値がゼロの要素はスキップ)
        if Xc[i,j] > cutoff:
            # ノード: 属性(外部変数等)とノードの大きさ(正規化した属性数)を保持する
            weights_n.append((attrs[i], {'weight': attr_counts[i] / attr_count_max, 'type': 'attr'}))
            # ノード: 単語とノードの大きさ(正規化した出現頻度)を保持する
            weights_n.append((words[j], {'weight': word_counts[j] / word_count_max, 'type': 'word'}))
            # エッジ: 属性(外部変数等)と単語を結ぶエッジの太さ(正規化した共起行列の値)を保持する
            weights_c.append((attrs[i], words[j], Xc[i,j] / Xc_max))

    # プロットの準備
    plt.figure(figsize=(width, height))

    # グラフの作成
    G = nx.Graph()
    G.add_nodes_from(weights_n)
    G.add_weighted_edges_from(weights_c)
    G.remove_nodes_from(list(nx.isolates(G)))
    # G = nx.minimum_spanning_tree(G)
    # pos = nx.spring_layout(G, k=0.3)
    pos = graphviz_layout(G, prog='neato', args='-Goverlap="scalexy" -Gsep="+6" -Gnodesep=0.8 -Gsplines="polyline" -GpackMode="graph" -Gstart={}'.format(43))

    # プロット用にノートとエッジの重みをリストに変換する
    nodelist_a = [node for node in G.nodes if G.nodes[node]['type'] == 'attr']
    nodelist_w = [node for node in G.nodes if G.nodes[node]['type'] == 'word']
    weights_a = np.array([G.nodes[node]['weight'] for node in G.nodes if G.nodes[node]['type'] == 'attr'])
    weights_w = np.array([G.nodes[node]['weight'] for node in G.nodes if G.nodes[node]['type'] == 'word'])
    weights_e = np.array(list(nx.get_edge_attributes(G, 'weight').values()))

    # 属性と単語を色分けする
    color_map = []
    for node in G:
        if G.nodes[node]['type'] == 'word':
            color_map.append(G.degree(node)+3)   # "+3"はカラーマップをシフトする調整値

    # グラフの描画
    nx.draw_networkx_nodes(G, pos, node_color='lightsalmon', alpha=0.7, cmap=plt.cm.Set2, node_size=1000 * weights_a, nodelist=nodelist_a, node_shape='s')
    nx.draw_networkx_nodes(G, pos, node_color=color_map, alpha=0.7, cmap=plt.cm.Set2, node_size=5000 * weights_w, nodelist=nodelist_w)
    nx.draw_networkx_edges(G, pos, edge_color='gray', edge_cmap=plt.cm.Blues, alpha=0.7, width=3 * weights_e)
    nx.draw_networkx_labels(G, pos, font_family='IPAexGothic')

    # プロットの仕上げ
    plt.axis("off")
    plt.show()


# 係り受けによる共起ネットワークを描画する
def plot_dependency_network(df, word_counts, cutoff, width=8, height=8):

    # 必要ライブラリのインポート
    import numpy as np
    import networkx as nx
    from networkx.algorithms import community
    import matplotlib.pyplot as plt
    import japanize_matplotlib
    from networkx.drawing.nx_agraph import graphviz_layout

    # ノートブック中にプロットするマジックコマンド
    %matplotlib inline

    # 共起行列の中身(numpy行列)を取り出す
    Xc = df.values

    # 共起行列中の最大値を求める
    Xc_max = Xc.max()

    # プロットする単語リストを取得する
    words = df.columns

    # プロットする単語の出現頻度の最大値を求める (正規化用)
    count_max = word_counts.max()

    weights_w, weights_c = [], []

    # 共起行列の要素ごとのループ
    for i, j in zip(*Xc.nonzero()):
        # 対角行列でかつ値がしきい値を超えるものを保持する (値がゼロの要素はスキップ)
        if i != j and Xc[i,j] > cutoff:
            # ノード: 一方の単語とノードの大きさ(正規化した出現頻度)を保持する
            weights_w.append((words[i], {'weight': word_counts[i] / count_max}))
            # ノード: 他方の単語とノードの大きさ(正規化した出現頻度)を保持する
            weights_w.append((words[j], {'weight': word_counts[j] / count_max}))
            # エッジ: 両端の単語を結ぶエッジの太さ(正規化した共起行列の値)を保持する
            weights_c.append((words[i], words[j], Xc[i,j] / Xc_max))

    # プロットの準備
    plt.figure(figsize=(width, height))

    # グラフの作成
    G = nx.DiGraph()
    G.add_nodes_from(weights_w)
    G.add_weighted_edges_from(weights_c)
    G.remove_nodes_from(list(nx.isolates(G)))
    # G = nx.minimum_spanning_tree(G)
    # pos = nx.spring_layout(G, k=0.3)
    pos = graphviz_layout(G, prog='neato', args='-Goverlap="scalexy" -Gsep="+6" -Gnodesep=0.8 -Gsplines="polyline" -GpackMode="graph" -Gstart={}'.format(43))

    # プロット用にノートとエッジの重みをリストに変換する
    weights_n = np.array(list(nx.get_node_attributes(G, 'weight').values()))
    weights_e = np.array(list(nx.get_edge_attributes(G, 'weight').values()))

    # サブグラフの検出
    communities = community.greedy_modularity_communities(G)
    color_map = []
    for node in G:
        for i, c in enumerate(communities):
            if node in c:
                color_map.append(i)

    # グラフの描画
    nx.draw_networkx_nodes(G, pos, node_color=color_map, alpha=0.7, cmap=plt.cm.Set2, node_size=5000 * weights_n)
    nx.draw_networkx_edges(G, pos, edge_color='gray', edge_cmap=plt.cm.Blues, alpha=0.7, width=3 * weights_e)
    nx.draw_networkx_labels(G, pos, font_family='IPAexGothic')

    # プロットの仕上げ
    plt.axis("off")
    plt.show()


# 対応分析の結果をプロットする
def plot_coresp(row_coord, col_coord, row_labels, col_labels, explained_inertia=None, width=8, height=8):

    # 必要ライブラリのインポート
    import matplotlib.pyplot as plt
    import japanize_matplotlib

    # ノートブック中にプロットするマジックコマンド
    %matplotlib inline

    # プロットの準備
    plt.figure(figsize=(width, height))

    # 行方向(外部変数)のプロット
    plt.plot(row_coord[:, 0], row_coord[:, 1], "*", color='red', alpha=0.5)
    for i, label in enumerate(row_labels):
        plt.text(row_coord[i, 0], row_coord[i, 1], label, color='red', ha='left', va='bottom')

    # 列方向(単語)のプロット
    plt.plot(col_coord[:, 0], col_coord[:, 1], "o", color='blue', alpha=0.5)
    for i, label in enumerate(col_labels):
        plt.text(col_coord[i, 0], col_coord[i, 1], label, color='blue', ha='left', va='bottom')

    # 原点を通る水平と垂直線を引く
    plt.axvline(0, linestyle='dashed', color='gray', alpha=0.5)
    plt.axhline(0, linestyle='dashed', color='gray', alpha=0.5)

    # 軸ラベルに寄与率を追記する
    if explained_inertia is not None:
        plt.xlabel(f"Dim 1 ({explained_inertia[0]:.3f}%)")
        plt.ylabel(f"Dim 2 ({explained_inertia[1]:.3f}%)")

    # プロットの仕上げ
    # plt.axis('equal')
    plt.show()


# PCA の結果をプロットする
def plot_pca(coeff, reduced, row_labels, col_labels, var_ratio=None, width=8, height=8):

    # 必要ライブラリのインポート
    import matplotlib.pyplot as plt
    import japanize_matplotlib

    # ノートブック中にプロットするマジックコマンド
    %matplotlib inline

    # プロットの準備
    plt.figure(figsize=(width, height))

    # 行方向(外部変数)のプロット
    for i, label in enumerate(row_labels):
        plt.arrow(0, 0, coeff[i,0], coeff[i,1], color='r', alpha=0.5)
        plt.text(coeff[i, 0], coeff[i, 1], label, color='red', ha='left', va='bottom')

    # 列方向(単語)のプロット
    plt.plot(reduced[:, 0], reduced[:, 1], "o", color='blue', alpha=0.5)
    for i, label in enumerate(col_labels):
        plt.text(reduced[i, 0], reduced[i, 1], label, color='blue', ha='left', va='bottom')

    # 原点を通る水平と垂直線を引く
    plt.axvline(0, linestyle='dashed', color='gray', alpha=0.5)
    plt.axhline(0, linestyle='dashed', color='gray', alpha=0.5)

    # 軸ラベルに寄与率を追記する
    if var_ratio is not None:
        plt.xlabel(f"Dim 1 ({var_ratio[0]*100:.3f}%)")
        plt.ylabel(f"Dim 2 ({var_ratio[1]*100:.3f}%)")

    # プロットの仕上げ
    # plt.axis('equal')
    plt.show()


# 共起頻度行列を Jaccard 係数行列に変換する (抽出語-抽出語用)
def jaccard_coef(cooccur_df, cross_df):

    # 必要ライブラリのインポート
    import numpy as np
    import pandas as pd

    # 共起行列の中身(numpy行列)を取り出す
    Xc = cooccur_df.values

    # Jaccard 係数行列を初期化する (共起行列と同じ形)
    Xj = np.zeros(Xc.shape)

    # 単語ごとに共起度を集計する
    Xc_sum = cross_df.sum(axis=0).values

    # 共起行列の要素ごとのループ (値がゼロの要素はスキップ)
    for i, j in zip(*Xc.nonzero()):
        # 対角行列の要素を取り出す
        if i < j:
            # Jaccard 係数を求める
            Xj[i,j] = Xc[i,j] / (Xc_sum[i] + Xc_sum[j] - Xc[i,j])

    # DataFrame 型に整える
    jaccard_df = pd.DataFrame(Xj, columns=cooccur_df.columns, index=cooccur_df.columns)

    return jaccard_df


# 共起頻度行列を Jaccard 係数行列に変換する (外部変数-抽出語用)
def jaccard_attrs_coef(df, attr_counts, word_counts, total=10000, conditional=False):

    # 必要ライブラリのインポート
    import numpy as np
    import pandas as pd

    # 共起行列の中身(numpy行列)を取り出す
    Xc = df.values

    # Jaccard 係数行列を初期化する (共起行列と同じ形)
    Xj = np.zeros(df.shape)

    # 共起行列の要素ごとのループ (値がゼロの要素はスキップ)
    for i, j in zip(*Xc.nonzero()):

        # conditional フラグが True の場合, 条件付き確率 > 前提確率 以外はゼロにする
        if not conditional:

            # 条件付き確率を求める
            conditional_prob = Xc[i,j] / attr_counts[i]

            # 前提確率を求める
            assumption_prob = word_counts[j] / total

            # 条件付き確率 > 前提確率の場合
            if conditional_prob > assumption_prob:
                # Jaccard 係数を求める
                Xj[i,j] = Xc[i,j] / (attr_counts[i] + word_counts[j] - Xc[i,j])

            # 条件付き確率 <= 前提確率の場合
            else:
                # ゼロにする
                Xj[i,j] = .0

        # conditional フラグが False の場合, すべてのケースで Jaccard 係数を求める (デフォルト)
        else:
            # Jaccard 係数を求める
            Xj[i,j] = Xc[i,j] / (attr_counts[i] + word_counts[j] - Xc[i,j])

    # DataFrame 型に整える
    jaccard_df = pd.DataFrame(Xj, columns=df.columns, index=df.index)

    return jaccard_df

1.1 データのダウンロード (前回ダウンロード済みのためスキップ)¶

以下のデータがダウンロード済みです

ファイル名 件数 データセット 備考
rakuten-1000-2022-2023.xlsx.zip 10,000 •レジャー+ビジネスの 10エリア
•エリアごと 1,000件 (ランダムサンプリング)
•期間: 2022/1~2023 GW明け
本講義の全体を通して使用する
In [ ]:
# もし、再度ダウンロードが必要な場合は残りの行のコメントマーク「#」を除去して、このセルを再実行してください

# FILE_ID = "1n-uvGoH7XQhxexN57hYXuFrkGeHKp-HV"
# !gdown --id {FILE_ID}
# !unzip rakuten-1000-2022-2023.xlsx.zip

1.2 データの読み込み (DataFrame型)¶

In [ ]:
import numpy as np
import pandas as pd

all_df = pd.read_excel("rakuten-1000-2022-2023.xlsx")
print(all_df.shape)
display(all_df.head())
(10000, 18)
カテゴリー エリア 施設番号 施設名 コメント 総合 サービス 立地 部屋 設備・アメニティ 風呂 食事 旅行の目的 同伴者 宿泊年月 投稿者 年代 性別
0 A_レジャー 01_登別 5500 登別 石水亭 お風呂が男女時間交代の屋上風呂に入れなかったのが残念でした。食事はスタッフの皆さんがとても親... 5 5 4 5 5.0 4.0 5.0 レジャー 家族 44743 さとちん4359 60代 女性
1 A_レジャー 01_登別 39175 登別温泉 登別グランドホテル 大浴場のアメニティ、洗い場のクレンジング、ソープ、パック等が充実していて、大変良かったです。... 4 5 4 3 5.0 5.0 5.0 レジャー 家族 44835 まなちゃん5695 60代 女性
2 A_レジャー 01_登別 20547 北湯沢温泉郷 湯元 ホロホロ山荘 犬プラン素泊まり一名で宿泊しましたが、夜8時頃売店でお土産(その時飲む飲み物など)を買う時に... 1 1 2 2 3.0 4.0 NaN レジャー 一人 44835 投稿者 na na
3 A_レジャー 01_登別 139962 ザ レイクビュー TOYA 乃の風リゾート 部屋風呂が最高でした。 5 5 5 5 5.0 5.0 5.0 レジャー 恋人 44986 投稿者 na na
4 A_レジャー 01_登別 80732 登別カルルス温泉 湯元オロフレ荘 カルルス温泉郷の静かな立地、そして湯元の素晴らしい泉質、美味しいお料理、飾らない中にも親切な... 5 5 5 5 5.0 5.0 5.0 レジャー 家族 44986 投稿者 na na

1.3 「文書-抽出語」表 を作成する¶

コメント列から単語を抽出する (単語を品詞「名詞」「形容詞」「未知語」で絞り込む)

In [ ]:
# 必要ライブラリのインポート
from collections import defaultdict
import MeCab

# mecab の初期化
tagger = MeCab.Tagger("-r ../tools/usr/local/etc/mecabrc --unk-feature 未知語")

# 単語頻度辞書の初期化
word_counts = defaultdict(lambda: 0)

# 抽出語情報リストの初期化
words = []

# 半角->全角変換マクロを定義する
ZEN = "".join(chr(0xff01 + i) for i in range(94))
HAN = "".join(chr(0x21 + i) for i in range(94))
HAN2ZEN = str.maketrans(HAN, ZEN)

# ストップワードを定義する
# stopwords = ['する', 'ある', 'ない', 'いう', 'もの', 'こと', 'よう', 'なる', 'ほう']
stopwords = ["湯畑"]

# データ1行ごとのループ
for index, row in all_df.iterrows():

    # 半角->全角変換した後で, mecab で解析する
    node = tagger.parseToNode(row["コメント"].translate(HAN2ZEN))

    # 形態素ごとのループ
    while node:
        # 解析結果を要素ごとにバラす
        features = node.feature.split(',')

        # 品詞1 を取り出す
        pos1 = features[0]

        # 品詞2 を取り出す
        pos2 = features[1] if len(features) > 1 else ""

        # 原形 を取り出す
        base = features[6] if len(features) > 6 else None

        # 原型がストップワードに含まれない単語のみ抽出する
        if base not in stopwords:

            # 「名詞-一般」
            if (pos1 == "名詞" and pos2 == "一般"):
                base = base if base is not None else node.surface
                postag = "名詞"
                key = (base, postag)

                # 単語頻度辞書をカウントアップする
                word_counts[key] += 1

                # 抽出語情報をリストに追加する
                words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])

            # 「形容動詞」
            elif (pos1 == "名詞" and pos2 == "形容動詞語幹"):
                base = base if base is not None else node.surface
                base = f"{base}だ"
                postag = "形容動詞"
                key = (base, postag)

                # 単語頻度辞書をカウントアップする
                word_counts[key] += 1

                # 抽出語情報をリストに追加する
                words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])

            # 「形容詞」
            elif pos1 == "形容詞":
                base = base if base is not None else node.surface
                postag = "形容詞"
                key = (base, postag)

                # 単語頻度辞書をカウントアップする
                word_counts[key] += 1

                # 抽出語情報をリストに追加する
                words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])

            # 「未知語」
            elif pos1 == "未知語":
                base = base if base is not None else node.surface
                postag = "未知語"
                key = (base, postag)

                # 単語頻度辞書をカウントアップする
                word_counts[key] += 1

                # 抽出語情報をリストに追加する
                words.append([index + 1, base, postag, row["カテゴリー"], row["エリア"], key])

        # 次の形態素へ
        node = node.next

# DataFrme 型に整える
columns = [
    "文書ID",
    # "単語ID",
    "表層",
    "品詞",
    "カテゴリー",
    "エリア",
    "dict_key",
]
docs_df = pd.DataFrame(words, columns=columns)

# DataFrame を表示する
print(docs_df.shape)
display(docs_df.head())
(149372, 6)
文書ID 表層 品詞 カテゴリー エリア dict_key
0 1 風呂 名詞 A_レジャー 01_登別 (風呂, 名詞)
1 1 男女 名詞 A_レジャー 01_登別 (男女, 名詞)
2 1 屋上 名詞 A_レジャー 01_登別 (屋上, 名詞)
3 1 風呂 名詞 A_レジャー 01_登別 (風呂, 名詞)
4 1 残念だ 形容動詞 A_レジャー 01_登別 (残念だ, 形容動詞)

抽出語の出現頻度をカウントする

In [ ]:
word_list = []
for i, (k, v) in enumerate(sorted(word_counts.items(), key=lambda x:x[1], reverse=True)):
    word_list.append((i, k[0], v, k))

columns = [
    "単語ID",
    "表層",
    "出現頻度",
    "dict_key"
]
word_counts_df = pd.DataFrame(word_list, columns=columns)
print(word_counts_df.shape)
display(word_counts_df.head(10))
(8390, 4)
単語ID 表層 出現頻度 dict_key
0 0 部屋 6689 (部屋, 名詞)
1 1 良い 5257 (良い, 形容詞)
2 2 ホテル 2831 (ホテル, 名詞)
3 3 風呂 2702 (風呂, 名詞)
4 4 美味しい 2249 (美味しい, 形容詞)
5 5 ない 2124 (ない, 形容詞)
6 6 スタッフ 1712 (スタッフ, 名詞)
7 7 温泉 1705 (温泉, 名詞)
8 8 よい 1446 (よい, 形容詞)
9 9 立地 1374 (立地, 名詞)

単語IDを紐つける (出現回数 Top 150語のみ抽出する)

In [ ]:
word_counts_150_df = word_counts_df[0:150]
merged_df = pd.merge(docs_df, word_counts_150_df, how="inner", on="dict_key", suffixes=["", "_right"])
docs_150_df = merged_df[["文書ID", "単語ID", "表層", "品詞", "カテゴリー", "エリア", "dict_key"]]
print(docs_150_df.shape)
display(docs_150_df)
(83459, 7)
文書ID 単語ID 表層 品詞 カテゴリー エリア dict_key
0 1 3 風呂 名詞 A_レジャー 01_登別 (風呂, 名詞)
1 1 3 風呂 名詞 A_レジャー 01_登別 (風呂, 名詞)
2 4 3 風呂 名詞 A_レジャー 01_登別 (風呂, 名詞)
3 8 3 風呂 名詞 A_レジャー 01_登別 (風呂, 名詞)
4 15 3 風呂 名詞 A_レジャー 01_登別 (風呂, 名詞)
... ... ... ... ... ... ... ...
83454 9933 149 スペース 名詞 B_ビジネス 10_福岡 (スペース, 名詞)
83455 9937 149 スペース 名詞 B_ビジネス 10_福岡 (スペース, 名詞)
83456 9943 149 スペース 名詞 B_ビジネス 10_福岡 (スペース, 名詞)
83457 9980 149 スペース 名詞 B_ビジネス 10_福岡 (スペース, 名詞)
83458 9980 149 スペース 名詞 B_ビジネス 10_福岡 (スペース, 名詞)

83459 rows × 7 columns

「文書-抽出語」表 を作成する

In [ ]:
word_counts_75_df = word_counts_df[0:75]
merged_df = pd.merge(docs_df, word_counts_75_df, how="inner", on="dict_key", suffixes=["", "_right"])
docs_75_df = merged_df[["文書ID", "単語ID", "表層", "品詞", "カテゴリー", "エリア", "dict_key"]]

cross_75_df = pd.crosstab(
    [
        docs_75_df['カテゴリー'], 
        docs_75_df['エリア'], 
        docs_75_df['文書ID']
    ], 
    docs_75_df['単語ID'], margins=False
)
cross_75_df.columns = word_counts_75_df["表層"]
print(cross_75_df.shape)
display(cross_75_df)
(9630, 75)
表層 部屋 良い ホテル 風呂 美味しい ない スタッフ 温泉 よい 立地 ... 新しい 楽しい 気持ち 雰囲気 女性 高い 建物 すごい 大きい 量
カテゴリー エリア 文書ID
A_レジャー 01_登別 1 0 0 0 2 1 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 0 1 0 0 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 1 1 0 0 0 1 0 1 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 1 0 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
5 0 0 0 0 1 0 0 0 0 1 ... 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
B_ビジネス 10_福岡 9996 0 1 0 0 0 1 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
9997 1 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 1 0
9998 0 0 0 0 1 0 0 0 1 0 ... 0 0 0 0 0 0 0 0 0 0
9999 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
10000 0 2 1 0 0 0 1 0 0 0 ... 0 0 0 0 0 1 0 0 0 0

9630 rows × 75 columns

「文書-抽出語」 表を {0,1} に変換する

In [ ]:
cross_75_df[cross_75_df > 0] = 1
print(cross_75_df.shape)
display(cross_75_df)
(9630, 75)
表層 部屋 良い ホテル 風呂 美味しい ない スタッフ 温泉 よい 立地 ... 新しい 楽しい 気持ち 雰囲気 女性 高い 建物 すごい 大きい 量
カテゴリー エリア 文書ID
A_レジャー 01_登別 1 0 0 0 1 1 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 0 1 0 0 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 1 1 0 0 0 1 0 1 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 1 0 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
5 0 0 0 0 1 0 0 0 0 1 ... 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
B_ビジネス 10_福岡 9996 0 1 0 0 0 1 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
9997 1 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 1 0
9998 0 0 0 0 1 0 0 0 1 0 ... 0 0 0 0 0 0 0 0 0 0
9999 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
10000 0 1 1 0 0 0 1 0 0 0 ... 0 0 0 0 0 1 0 0 0 0

9630 rows × 75 columns

1.4 共起ネットワーク¶

共起度行列を作成する (抽出語-抽出語)

In [ ]:
from scipy.sparse import csc_matrix

X = cross_75_df.values
X = csc_matrix(X)
Xc = (X.T * X)
Xc = np.triu(Xc.toarray())

cooccur_75_df = pd.DataFrame(Xc, columns=cross_75_df.columns, index=cross_75_df.columns)
print(cooccur_75_df.shape)
display(cooccur_75_df.head())
(75, 75)
表層 部屋 良い ホテル 風呂 美味しい ない スタッフ 温泉 よい 立地 ... 新しい 楽しい 気持ち 雰囲気 女性 高い 建物 すごい 大きい 量
表層
部屋 4568 1961 1091 1273 1068 1012 783 706 676 691 ... 200 175 192 190 183 187 177 190 186 174
良い 0 3701 892 1026 896 692 712 638 339 701 ... 140 121 201 175 142 136 140 133 132 168
ホテル 0 0 2041 391 354 506 411 235 267 356 ... 120 70 101 80 102 102 53 78 73 56
風呂 0 0 0 2143 598 503 376 359 333 317 ... 83 102 112 100 101 84 93 77 134 120
美味しい 0 0 0 0 1962 379 432 429 229 215 ... 67 118 92 97 74 69 85 93 81 142

5 rows × 75 columns

(a) 共起ネットワーク (共起度の高いエッジを残す)¶

In [ ]:
word_counts = cross_75_df.sum(axis=0).values
plot_cooccur_network(cooccur_75_df, word_counts, cooccur_75_df.values.max() * 0.05)
<Figure size 800x800 with 0 Axes>

(b) 共起ネットワーク (Jaccard 係数が上位のエッジを残す)¶

In [ ]:
jaccard_75_df = jaccard_coef(cooccur_75_df, cross_75_df)

word_counts = cross_75_df.sum(axis=0).values
plot_cooccur_network(jaccard_75_df, word_counts, np.sort(jaccard_75_df.values.reshape(-1))[::-1][60])
<Figure size 800x800 with 0 Axes>

1.5 対応分析¶

「文書-抽出語」 表を確認する

In [ ]:
print(cross_75_df.shape)
display(cross_75_df.head())
(9630, 75)
表層 部屋 良い ホテル 風呂 美味しい ない スタッフ 温泉 よい 立地 ... 新しい 楽しい 気持ち 雰囲気 女性 高い 建物 すごい 大きい 量
カテゴリー エリア 文書ID
A_レジャー 01_登別 1 0 0 0 1 1 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 0 1 0 0 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 1 1 0 0 0 1 0 1 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 1 0 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
5 0 0 0 0 1 0 0 0 0 1 ... 0 0 0 0 0 0 0 0 0 0

5 rows × 75 columns

「外部変数-抽出語」 クロス集計表を作成する

In [ ]:
aggregate_75_df = pd.concat([
    cross_75_df.groupby(level='カテゴリー').sum(), 
    cross_75_df.groupby(level='エリア').sum()
    ])
print(aggregate_75_df.shape)
display(aggregate_75_df)
(12, 75)
表層 部屋 良い ホテル 風呂 美味しい ない スタッフ 温泉 よい 立地 ... 新しい 楽しい 気持ち 雰囲気 女性 高い 建物 すごい 大きい 量
A_レジャー 2398 2046 757 1535 1430 880 888 1188 631 518 ... 136 251 202 218 127 161 215 161 166 260
B_ビジネス 2170 1655 1284 608 532 749 523 110 574 810 ... 187 65 113 87 160 134 87 117 123 30
01_登別 447 409 194 323 255 187 148 222 114 38 ... 25 42 32 26 22 33 54 38 42 24
02_草津 488 434 181 352 274 180 154 275 117 155 ... 23 53 43 33 25 30 53 28 34 57
03_箱根 548 436 134 326 355 202 212 212 133 57 ... 30 62 43 55 36 35 54 31 31 73
04_道後 416 349 191 181 174 130 135 225 137 176 ... 37 32 31 29 13 28 32 25 28 29
05_湯布院 499 418 57 353 372 181 239 254 130 92 ... 21 62 53 75 31 35 22 39 31 77
06_札幌 452 346 255 121 129 151 114 38 103 166 ... 34 16 15 24 28 30 12 18 34 6
07_名古屋 434 310 241 116 97 144 102 18 133 141 ... 40 13 22 13 42 28 14 17 25 3
08_東京 441 338 240 106 99 131 99 12 104 166 ... 36 14 26 9 30 18 19 25 19 10
09_大阪 431 317 297 135 88 162 93 20 121 161 ... 32 10 27 24 37 26 20 31 19 3
10_福岡 412 344 251 130 119 161 115 22 113 176 ... 45 12 23 17 23 32 22 26 26 8

12 rows × 75 columns

(a) 対応分析プロット (ライブラリ mca を使用)¶

In [ ]:
import mca

ncols = aggregate_75_df.shape[1]
mca_ben = mca.MCA(aggregate_75_df, ncols=ncols, benzecri=False)
row_coord = mca_ben.fs_r(N=2)
col_coord = mca_ben.fs_c(N=2)

eigenvalues = mca_ben.L
total = np.sum(eigenvalues)
explained_inertia = 100 * eigenvalues / total

row_labels = aggregate_75_df.index
col_labels = aggregate_75_df.columns
plot_coresp(row_coord, col_coord,row_labels, col_labels, explained_inertia)

(b) 対応分析プロット (カイ2乗値を手計算してプロットする)¶

In [ ]:
table_N = aggregate_75_df.values
row_sum = table_N.sum(axis=1)
col_sum = table_N.sum(axis=0)
n = aggregate_75_df.values.sum()

expected = np.outer(row_sum, col_sum) / n
chisq = np.square(table_N - expected) / expected
residuals = (table_N - expected) / np.sqrt(expected)

# Standardized residuals
residuals = residuals / np.sqrt(n)

# Number of dimensions
nb_axes = min(residuals.shape[0]-1, residuals.shape[1]-1)

# Singular value decomposition
U, s, V = np.linalg.svd(residuals, full_matrices=True)
sv = s[:nb_axes]
u = U[:, :nb_axes]
v = V[:nb_axes, :]

# row mass
row_mass = row_sum / n

# row coord = u * sv /sqrt(row.mass)
row_coord = (u * sv) / np.sqrt(row_mass)[:, np.newaxis]

# col mass
col_mass = col_sum / n

# row coord = sv * vT /sqrt(col.mass)
col_coord = (sv * v.T) / np.sqrt(col_mass)[:, np.newaxis]

# eige nvalue
eige_nvalue = s ** 2

# contribution rate 
explained_inertia = 100 * eige_nvalue / sum(eige_nvalue)

row_labels = aggregate_75_df.index
col_labels = aggregate_75_df.columns
plot_coresp(row_coord, col_coord,row_labels, col_labels, explained_inertia)

(c) 対応分析プロット (共起頻度のままプロットする)¶

In [ ]:
import numpy as np

table_N = aggregate_75_df.values
table_P = table_N / aggregate_75_df.values.max()

# Singular value decomposition
U, s, V = np.linalg.svd(table_P, full_matrices=True)
sv = s[:nb_axes]
u = U[:, :nb_axes]
v = V[:nb_axes, :]

# row mass
row_mass = row_sum / n

# row coord = u * sv /sqrt(row.mass)
row_coord = (u * sv) / np.sqrt(row_mass)[:, np.newaxis]

# col mass
col_mass = col_sum / n

# row coord = sv * vT /sqrt(col.mass)
col_coord = (sv * v.T) / np.sqrt(col_mass)[:, np.newaxis]

# eige nvalue
eige_nvalue = s ** 2

# contribution rate 
explained_inertia = 100 * eige_nvalue / sum(eige_nvalue)

row_labels = aggregate_75_df.index
col_labels = aggregate_75_df.columns
plot_coresp(row_coord, col_coord,row_labels, col_labels, explained_inertia)

(d) PCAプロット (共起頻度のままプロットする)¶

In [ ]:
from sklearn.decomposition import PCA

table_N = aggregate_75_df.values

pca = PCA()

reduced = pca.fit_transform(table_N.T)
coeff = np.transpose(pca.components_)
var_ratio = pca.explained_variance_ratio_

scalex = 1.0 / (reduced[:,0].max() - reduced[:,0].min())
scaley = 1.0 / (reduced[:,1].max() - reduced[:,1].min())
reduced[:,0] *= scalex
reduced[:,1] *= scaley

plot_pca(coeff, reduced, row_labels, col_labels, var_ratio)

(e) PCAプロット (カイ2乗値をプロットする)¶

In [ ]:
from sklearn.decomposition import PCA

import numpy as np

table_N = aggregate_75_df.values
row_sum = table_N.sum(axis=1)
col_sum = table_N.sum(axis=0)
n = aggregate_75_df.values.sum()

expected = np.outer(row_sum, col_sum) / n
chisq = np.square(table_N - expected) / expected
residuals = (table_N - expected) / np.sqrt(expected)

# Standardized residuals
residuals = residuals / np.sqrt(n)

pca = PCA()

reduced = pca.fit_transform(residuals.T)
coeff = np.transpose(pca.components_)
var_ratio = pca.explained_variance_ratio_

scalex = 1.0 / (reduced[:,0].max() - reduced[:,0].min())
scaley = 1.0 / (reduced[:,1].max() - reduced[:,1].min())
reduced[:,0] *= scalex
reduced[:,1] *= scaley

plot_pca(coeff, reduced, row_labels, col_labels, var_ratio)

2. テキスト分析 (実践編)¶

2.1 カテゴリーやエリアごとのユーザーの注目ポイントを押さえる¶

2.1.1 「文書-抽出語」表の作成¶

「文書-抽出語」表を作成する (出現回数 Top 1000語)

In [ ]:
word_counts_1000_df = word_counts_df[0:1000]
merged_df = pd.merge(docs_df, word_counts_1000_df, how="inner", on="dict_key", suffixes=["", "_right"])
docs_1000_df = merged_df[["文書ID", "単語ID", "表層", "品詞", "カテゴリー", "エリア", "dict_key"]]

cross_1000_df = pd.crosstab(
    [
        docs_1000_df['カテゴリー'], 
        docs_1000_df['エリア'], 
        docs_1000_df['文書ID']
    ], 
    docs_1000_df['単語ID'], margins=False
)
cross_1000_df.columns = word_counts_1000_df["表層"]

「文書-抽出語」表を {0,1} に変換する

In [ ]:
cross_1000_df[cross_1000_df > 0] = 1
print(cross_1000_df.shape)
display(cross_1000_df)
(9879, 1000)
表層 部屋 良い ホテル 風呂 美味しい ない スタッフ 温泉 よい 立地 ... 長湯 懐かしい 人柄 人達 アンケート トータル 白 レンタカー ダメだ スポット
カテゴリー エリア 文書ID
A_レジャー 01_登別 1 0 0 0 1 1 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 0 1 0 0 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 1 1 0 0 0 1 0 1 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 1 0 0 1 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
5 0 0 0 0 1 0 0 0 0 1 ... 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
B_ビジネス 10_福岡 9996 0 1 0 0 0 1 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
9997 1 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
9998 0 0 0 0 1 0 0 0 1 0 ... 0 0 0 0 0 0 0 0 0 0
9999 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
10000 0 1 1 0 0 0 1 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

9879 rows × 1000 columns

2.1.2 共起行列を作成する (外部変数-抽出語)¶

In [ ]:
aggregate_df = pd.concat(
    [
        cross_1000_df.groupby(level='カテゴリー').sum(), 
        cross_1000_df.groupby(level='エリア').sum()
    ]
)
print(aggregate_df.shape)
display(aggregate_df)
(12, 1000)
表層 部屋 良い ホテル 風呂 美味しい ない スタッフ 温泉 よい 立地 ... 長湯 懐かしい 人柄 人達 アンケート トータル 白 レンタカー ダメだ スポット
A_レジャー 2398 2046 757 1535 1430 880 888 1188 631 518 ... 18 18 18 13 9 8 13 6 11 13
B_ビジネス 2170 1655 1284 608 532 749 523 110 574 810 ... 1 1 1 6 6 11 4 13 8 6
01_登別 447 409 194 323 255 187 148 222 114 38 ... 6 4 1 3 3 3 2 1 1 1
02_草津 488 434 181 352 274 180 154 275 117 155 ... 4 7 8 4 3 2 2 1 2 1
03_箱根 548 436 134 326 355 202 212 212 133 57 ... 4 2 2 3 1 1 5 1 5 1
04_道後 416 349 191 181 174 130 135 225 137 176 ... 3 1 3 1 0 0 1 2 2 2
05_湯布院 499 418 57 353 372 181 239 254 130 92 ... 1 4 4 2 2 2 3 1 1 8
06_札幌 452 346 255 121 129 151 114 38 103 166 ... 0 0 1 1 1 2 0 7 4 1
07_名古屋 434 310 241 116 97 144 102 18 133 141 ... 0 0 0 1 2 0 2 1 1 1
08_東京 441 338 240 106 99 131 99 12 104 166 ... 0 0 0 2 0 1 1 0 0 1
09_大阪 431 317 297 135 88 162 93 20 121 161 ... 1 1 0 2 3 5 1 0 1 1
10_福岡 412 344 251 130 119 161 115 22 113 176 ... 0 0 0 0 0 3 0 5 2 2

12 rows × 1000 columns

2.1.3 Jaccard 係数を求める (外部変数-抽出語)¶

In [ ]:
word_counts = cross_1000_df.sum(axis=0).values
attr_counts = np.hstack(
    [
        all_df.value_counts('カテゴリー').values,
        all_df.value_counts('エリア').values
    ]
)
jaccard_attrs_df = jaccard_attrs_coef(aggregate_df, attr_counts, word_counts, total=10000, conditional=False)
print(jaccard_attrs_df.shape)
display(jaccard_attrs_df)
(12, 1000)
表層 部屋 良い ホテル 風呂 美味しい ない スタッフ 温泉 よい 立地 ... 長湯 懐かしい 人柄 人達 アンケート トータル 白 レンタカー ダメだ スポット
A_レジャー 0.334449 0.307438 0.000000 0.273716 0.258496 0.153070 0.160782 0.232485 0.113204 0.000000 ... 0.003599 0.003599 0.003599 0.002597 0.001798 0.000000 0.002598 0.000000 0.002196 0.002597
B_ビジネス 0.000000 0.000000 0.223033 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.146792 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.002196 0.000000 0.002597 0.000000 0.000000
01_登別 0.000000 0.095294 0.000000 0.114539 0.094200 0.076577 0.065400 0.106936 0.000000 0.000000 ... 0.005923 0.003941 0.000000 0.002953 0.002964 0.002953 0.001970 0.000000 0.000000 0.000000
02_草津 0.096063 0.101711 0.000000 0.126120 0.101935 0.073499 0.068232 0.135937 0.000000 0.071330 ... 0.003941 0.006917 0.007913 0.003941 0.002964 0.001967 0.001970 0.000000 0.001967 0.000000
03_箱根 0.109163 0.102227 0.000000 0.115726 0.136172 0.083230 0.096407 0.101630 0.064189 0.000000 ... 0.003941 0.001967 0.001967 0.002953 0.000000 0.000000 0.004941 0.000000 0.004931 0.000000
04_道後 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.108538 0.066248 0.081784 ... 0.002953 0.000000 0.002953 0.000000 0.000000 0.000000 0.000000 0.001967 0.001967 0.001967
05_湯布院 0.098442 0.097595 0.000000 0.126523 0.143629 0.073938 0.110037 0.124266 0.062651 0.000000 ... 0.000000 0.003941 0.003941 0.001967 0.001974 0.001967 0.002959 0.000000 0.000000 0.007913
06_札幌 0.000000 0.000000 0.091529 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.076781 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.001967 0.000000 0.006917 0.003941 0.000000
07_名古屋 0.000000 0.000000 0.086071 0.000000 0.000000 0.000000 0.000000 0.000000 0.064189 0.064472 ... 0.000000 0.000000 0.000000 0.000000 0.001974 0.000000 0.001970 0.000000 0.000000 0.000000
08_東京 0.000000 0.000000 0.085684 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.076781 ... 0.000000 0.000000 0.000000 0.001967 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
09_大阪 0.000000 0.000000 0.108236 0.000000 0.000000 0.000000 0.000000 0.000000 0.058061 0.074296 ... 0.000000 0.000000 0.000000 0.001967 0.002964 0.004931 0.000000 0.000000 0.000000 0.000000
10_福岡 0.000000 0.000000 0.089964 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.081784 ... 0.000000 0.000000 0.000000 0.000000 0.000000 0.002953 0.000000 0.004931 0.001967 0.001967

12 rows × 1000 columns

2.1.4 特徴語ランキング¶

In [ ]:
df_list = []
for index, row in jaccard_attrs_df.iterrows():
    df_list.append(row.iloc[np.argsort(row.values)[::-1][:10]].reset_index())

ranking_df = pd.DataFrame(pd.concat(df_list, axis=1))
ranking_df.columns = np.array([c for pair in [[x,f"jaccard"] for x in jaccard_attrs_df.index] for c in pair], dtype='object')
display(ranking_df)
A_レジャー jaccard B_ビジネス jaccard 01_登別 jaccard 02_草津 jaccard 03_箱根 jaccard ... 06_札幌 jaccard 07_名古屋 jaccard 08_東京 jaccard 09_大阪 jaccard 10_福岡 jaccard
0 部屋 0.334449 ホテル 0.223033 風呂 0.114539 温泉 0.135937 美味しい 0.136172 ... ホテル 0.091529 ホテル 0.086071 駅 0.102041 ホテル 0.108236 ホテル 0.089964
1 良い 0.307438 立地 0.146792 温泉 0.106936 風呂 0.126120 露天風呂 0.133758 ... 立地 0.076781 便利だ 0.071669 ホテル 0.085684 駅 0.095941 便利だ 0.086882
2 風呂 0.273716 便利だ 0.133602 良い 0.095294 宿 0.119559 風呂 0.115726 ... 便利だ 0.076490 駅 0.070913 便利だ 0.077703 便利だ 0.080135 立地 0.081784
3 美味しい 0.258496 駅 0.124465 美味しい 0.094200 美味しい 0.101935 部屋 0.109163 ... 綺麗だ 0.071357 綺麗だ 0.069208 立地 0.076781 立地 0.074296 駅 0.063881
4 温泉 0.232485 綺麗だ 0.123283 バイキング 0.089563 良い 0.101711 良い 0.102227 ... 浴場 0.069540 フロント 0.065673 近い 0.070529 綺麗だ 0.072435 フロント 0.062741
5 スタッフ 0.160782 フロント 0.106943 残念だ 0.078018 部屋 0.096063 温泉 0.101630 ... フロント 0.064498 立地 0.064472 綺麗だ 0.063872 フロント 0.067441 綺麗だ 0.060169
6 ない 0.153070 近い 0.091118 ない 0.076577 最高 0.090361 宿 0.096697 ... 広い 0.062713 よい 0.064189 快適だ 0.063002 快適だ 0.064317 トイレ 0.052975
7 宿 0.152066 快適だ 0.089507 夕食 0.075641 夕食 0.085382 スタッフ 0.096407 ... 快適だ 0.055828 近い 0.058531 コンビニ 0.058697 広い 0.064265 コンビニ 0.052669
8 露天風呂 0.144101 アメニティ 0.072060 種類 0.075444 ない 0.073499 夕食 0.094586 ... 駅 0.055687 アメニティ 0.056187 フロント 0.055191 近い 0.063164 いい 0.051754
9 最高 0.131658 コンビニ 0.069282 露天風呂 0.073583 大変だ 0.072688 ない 0.083230 ... ベッド 0.054865 快適だ 0.055181 アメニティ 0.051965 よい 0.058061 近い 0.050680

10 rows × 24 columns

2.1.5 ワードクラウド¶

In [ ]:
import matplotlib.pyplot as plt
%matplotlib inline

def sort_and_plot(name, group):
    # sort
    sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]
    group_cross_df = group.iloc[:,sorted_columns]

    # plot
    ax = fig.add_subplot(4, 3, i+1)
    plot_wordcloud_ax(ax, " ".join(group_cross_df.columns))
    ax.set_title(name)


fig = plt.figure(figsize=(16, 12))

i = 0
for name, group in cross_1000_df.groupby(level='カテゴリー'):
    sort_and_plot(name, group)
    i += 1

    for sub_name, sub_group in group.groupby(level='エリア'):
        sort_and_plot(sub_name, sub_group)
        i += 1

plt.tight_layout()
plt.show()

2.1.6 共起ネットワーク図¶

In [ ]:
import matplotlib.pyplot as plt
%matplotlib inline

def sort_and_plot(name, group):
    # sort
    sorted_columns = np.argsort(jaccard_attrs_df.loc[name].values)[::-1][:75]
    group_cross_df = group.iloc[:,sorted_columns]

    # plot
    X = group_cross_df.values
    X = csc_matrix(X)
    Xc = (X.T * X)
    Xc = np.triu(Xc.toarray())

    group_cooccur_df = pd.DataFrame(Xc, columns=group_cross_df.columns, index=group_cross_df.columns)
    group_jaccard_df = jaccard_coef(group_cooccur_df, group_cross_df)

    word_counts = group_cross_df.sum(axis=0).values

    ax = fig.add_subplot(4, 3, i+1)
    plot_cooccur_network_ax(ax, group_jaccard_df, word_counts, np.sort(group_jaccard_df.values.reshape(-1))[::-1][60])
    ax.set_title(name)


fig = plt.figure(figsize=(20, 28))

i = 0
for name, group in cross_1000_df.groupby(level='カテゴリー'):
    sort_and_plot(name, group)
    i += 1

    for sub_name, sub_group in group.groupby(level='エリア'):
        sort_and_plot(sub_name, sub_group)
        i += 1

plt.tight_layout()
plt.show()